跳到主要内容

Spring Bean 的杂项知识

Bean 的作用域有哪些?

ScopeDescription
singleton单例模式 (默认就是单例模式)
prototype原型模式 每次从容器中 get 的时候,都会产生一个新对象
request将单个 Bean 定义限定为单个 HTTP 请求的生命周期。
session将单个 Bean 定义限定为 HTTP 会话的生命周期。
application将单个Bean 定义限定为 ServletContext 的生命周期。
websocket将单个 Bean 定义限定为 WebSocket 的生命周期。

request、session、application、websocket 仅在支持 web 的 Spring 应用程序上下文中有效。

<bean id="student2" class="com.alsritter.pojo.Student" p:name="张三" scope="prototype"/>

因为使用了原型模式,所以每取得一次就创建一个

public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
Student student = (Student) context.getBean("student2");
Student student2 = (Student) context.getBean("student2");
System.out.println(student == student2);
}
-->
false

Bean 单例的线程安全

当多个线程操作同一个对象的时候,对这个对象的成员变量的写操作会存在线程安全问题,而容器本身并没有提供 Bean 的线程安全策略,因此可以说 Spring 容器中的 Bean 本身不具备线程安全的特性,但是具体还是要结合具体 scope 的 Bean 去研究

先从单例与原型 Bean 分别进行说明

原型 Bean

对于原型 Bean,每次创建一个新对象,也就是线程之间并不存在 Bean 共享,自然是不会有线程安全的问题。

单例 Bean

对于单例 Bean,所有线程都共享一个单例实例 Bean,因此是存在资源的竞争。如果单例 Bean,是一个无状态 Bean,也就是线程中的操作不会对 Bean 的成员执行查询以外的操作,那么这个单例 Bean 是线程安全的。比如 Spring mvc 的 Controller、Service、Dao 等,这些 Bean 大多是无状态的,只关注于方法本身。

下面是一个有状态的 Bean 示例:(经典的并发问题)

public class TestManagerImpl implements TestManager {  
private User user;
public void deleteUser(User e) throws Exception {
user = e; //1
prepareData(e);
}

public void prepareData(User e) throws Exception {
user = getUserByID(e.getId()); //2
//使用user.getId(); //3
}

}

如果该 Bean 配置为 singleton,会出现什么样的状况呢? 如果有2个用户访问,都调用到了该 Bean,假定为 user1、user2。

当 user1 调用到程序中的步骤1 的时候,该 Bean 的私有变量 user 被赋值为 user1,当 user1 的程序走到步骤2 的时候,该 Bean 的私有变量 user 被重新赋值为 user1_create,理想的状况,当 user1 走到 3步骤的时候,私有变量 user 应该为user1_create ;但如果在 user1 调用到 3步骤之前,user2 开始运行到了步骤1了,由于单态的资源共享,则私有变量 user 被修改为 user2;这种情况下,user1 的步骤3 用到的 user.getId() 实际用到是 user2 的对象。

如果需要在 Bean 里保存状态常见的有 2 种解决办法:

  • 在类中定义一个 ThreadLocal 成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐的一种方式)。
  • 改变 Bean 的作用域为 “prototype”:每次请求都会创建一个新的 bean 实例,自然不会存在线程安全问题。

@Component 和 @Bean 的区别

作用对象不同: @Component 注解作用于类,而 @Bean 注解作用于方法。

Spring 帮助我们管理 Bean 分为两个部分,一个是注册 Bean,一个装配 Bean。

完成这两个动作有三种方式,一种是使用自动配置的方式、一种是使用 JavaConfig 的方式,一种就是使用 XML 配置的方式。(注意,虽然后面开始使用注解了,但是早在 XML 时期就这么分了)

@Compent 作用就相当于 XML配置,@Component 通常是通过类路径扫描来自动侦测以及自动装配到 Spring 容器中(可以使用 @ComponentScan 注解定义要扫描的路径从中找出标识了需要装配的类自动装配到 Spring 的 bean 容器中)

@Component
public class Student {

private String name = "lkm";

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

@Bean 需要在配置类中使用,即类上需要加上 @Configuration 注解

@Configuration
public class WebSocketConfig {
@Bean
public Student student(){
return new Student();
}
}

相当于

<beans>
<bean id="student" class="com.temp.Student"/>
</beans>

两者都可以通过 @Autowired 装配

@Autowired
Student student;

那为什么有了 @Compent,还需要 @Bean 呢?

1、如果想要将 第三方库中的组件 装配到应用中,在这种情况下,是没有办法在它的类上添加 @Component 注解的,因此就不能使用自动化装配的方案了,但是这时可以使用 @Bean,当然也可以使用 XML 配置。

2、使用 @Bean 进行装配更加的灵活,如下代码

// 这个 status 会根据类型自动注入
@Bean
public OneService getService(status) {
switch (status) {
case 1:
return new serviceImpl1();
case 2:
return new serviceImpl2();
case 3:
return new serviceImpl3();
}
}

注册为 Bean 的注解

一般使用 @Autowired 注解自动装配 bean,要想把类标识成可用于 @Autowired 注解自动装配的 bean 的类,采用以下注解可实现:

  • @Component:通用的注解,可标注任意类为 Spring 组件。如果一个 Bean不知道属于哪个层,可以使用 @Component 注解标注。
  • @Repository: 对应持久层即 Dao 层,主要用于数据库相关操作。
  • @Service: 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao层。
  • @Controller: 对应 Spring MVC 控制层,主要用于接受用户请求并调用 Service 层返回数据给前端页面。

Bean 的一生

Spring Bean 的生命周期只有下面这四个阶段

  • 实例化 Instantiation:包装对象,执行构造方法,用于提前暴露实例解决循环依赖问题
  • 属性赋值 Populate:寻找并且注入依赖,依赖的 Bean 还会递归调用 getBean 方法获取)
  • 初始化 Initialization:调用自定义的初始化方法(不是构造方法,而是调用 <bean>init-method 属性指定的初始化方法))
  • 销毁 Destruction

实例化 -> 属性赋值 -> 初始化 -> 销毁

实例化和属性赋值对应构造方法和 setter 方法的注入,初始化和销毁是用户能自定义扩展的两个阶段。

Bean 的创建主要逻辑都在 doCreate() 方法中,逻辑很清晰,就是顺序调用以下三个方法,这三个方法与三个生命周期阶段一一对应

  • createBeanInstance() -> 实例化
  • populateBean() -> 属性赋值
  • initializeBean() -> 初始化

这个 doCreateBean 位于 org.springframework.beans.factory.support 包下

// 忽略了无关代码
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
throws BeanCreationException {

// Instantiate the bean.
BeanWrapper instanceWrapper = null;
if (instanceWrapper == null) {
// 实例化阶段!
instanceWrapper = createBeanInstance(beanName, mbd, args);
}

final Object bean = instanceWrapper.getWrappedInstance();

// Initialize the bean instance.
Object exposedObject = bean;
try {
// 属性赋值阶段!
populateBean(beanName, mbd, instanceWrapper);
// 初始化阶段!
exposedObject = initializeBean(beanName, exposedObject, mbd);
}

}

至于销毁,是在容器关闭时调用的,详见 ConfigurableApplicationContext#close()

注意:使用 final 修饰形参分两种情况,修饰基本类型时是为了让这个参数只读,修饰引用类型时是为了防止引用的地址被改变,否则会无法通过编译

Reference

参考资料 请别再问Spring Bean的生命周期了! 参考资料 javaguide Spring bean 参考资料 5.3 @Component 和 @Bean 的区别是什么? 参考资料 @Component 和 @Bean 的区别 参考资料 Spring 中的bean 是线程安全的吗?